SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
7 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
49.7 KB · 766 lines tsx
Raw Blame History
1import type { EventDTO, FacilityDetail } from "@dci/core";2import { ExternalLink } from "lucide-react";3import type { Metadata } from "next";4import Link from "next/link";5import type { ReactNode } from "react";6import { Breadcrumbs, type Crumb } from "@/components/entity/breadcrumbs";7import { EntityMap } from "@/components/entity/entity-map";8import { JsonLd } from "@/components/entity/json-ld";9import { ExtLink, KeyValueList, NotPublished } from "@/components/entity/kv";10import { ProvenanceTable, WithProvenance, provFor } from "@/components/entity/provenance";11import { SectionNav } from "@/components/entity/section-nav";12import { SourcesFooter } from "@/components/entity/sources-footer";13import { EventList } from "@/components/event-list";14import { CapacityHistoryChart, ClaimsTable, DataQualityPanel, Evidence, EvidenceProvider, NearbyInfrastructurePanel } from "@/components/evidence";15import { FacilityTable } from "@/components/facility-table";16import { ProjectList } from "@/components/project-list";17import { AiMark, Badge, ConfidenceBadge, EstimateMark, FlagBadges, PrecisionBadge, ScopeBadge, SemanticsLabel, StatusBadge, TypeBadge } from "@/components/ui/badge";18import { Page, Section } from "@/components/ui/card";19import { CountryFlag } from "@/components/ui/country-flag";20import { ProvenancePopover } from "@/components/ui/popover";21import { SourceBadge } from "@/components/ui/source-badge";22import { ErrorState, OfflineNotice } from "@/components/ui/states";23import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";24import { SourceHistory, TimelineList, type TimelineEntry } from "@/components/ui/timeline";25import { countryHref } from "@/lib/countries";26import { bestMw, fmtDate, fmtInt, fmtMw, fmtPct, placeLine, splitMw } from "@/lib/format";27import { CONFIDENCE_LABEL, EVENT_LABEL, PRECISION_LABEL, STATUS_LABEL, TYPE_LABEL, isImprecise } from "@/lib/labels";28import { routes } from "@/lib/routes";29import { NOT_FOUND_METADATA, absoluteUrl, compactJsonLd, pageMetadata } from "@/lib/seo";30import { getFacility, mwIsEstimate } from "./data";3132export const revalidate = 300;3334type Params = { params: Promise<{ slug: string }> };3536function describe(f: FacilityDetail): string {37  const { mw, kind } = bestMw(f);38  const parts = [`${f.name} — ${TYPE_LABEL[f.facilityType].toLowerCase()} data center${f.recordScope === "campus" ? " campus" : ""}`];39  if (f.operator) parts[0] += ` operated by ${f.operator.name}`;40  const place = placeLine(f);41  if (place !== "—") parts[0] += ` in ${place}`;42  parts.push(`Status: ${STATUS_LABEL[f.status].toLowerCase()}.`);43  if (mw !== null) parts.push(`${kind === "it" ? "Known IT capacity" : kind === "total" ? "Total power" : "Planned build-out"}: ${mwIsEstimate(f) ? "≈ " : ""}${fmtMw(mw)} (published figure).`);44  if (f.openedOn) parts.push(`Opened ${fmtDate(f.openedOn)}.`);45  parts.push("Every figure sourced, versioned and tracked on DataCenterIndex.");46  return parts.join(" ");47}4849export async function generateMetadata({ params }: Params): Promise<Metadata> {50  const { slug } = await params;51  const r = await getFacility(slug);52  if (!r.ok) return r.status === 404 ? NOT_FOUND_METADATA : { title: "Data center", robots: { index: false } };53  const f = r.data;54  const suffix = f.city ? ` (${f.city}${f.countryIso2 ? `, ${f.countryIso2}` : ""})` : "";55  return pageMetadata({ title: `${f.name}${suffix}`, description: describe(f), path: routes.facility(f.slug) });56}5758/* ------------------------------------------------------------------ helpers */5960const EXTERNAL_ID_LINKS: Record<string, { label: string; href: (v: string) => string }> = {61  wikidata: { label: "Wikidata", href: (v) => `https://www.wikidata.org/wiki/${v}` },62  wikipedia_en: { label: "Wikipedia", href: (v) => (v.startsWith("http") ? v : `https://en.wikipedia.org/wiki/${v}`) },63  peeringdb_fac: { label: "PeeringDB facility", href: (v) => `https://www.peeringdb.com/fac/${v}` },64  peeringdb: { label: "PeeringDB", href: (v) => `https://www.peeringdb.com/fac/${v}` },65  osm: { label: "OpenStreetMap", href: (v) => (v.startsWith("http") ? v : `https://www.openstreetmap.org/${v.replace(/^([nwr])(\d+)$/, (_, t: string, id: string) => `${t === "n" ? "node" : t === "w" ? "way" : "relation"}/${id}`)}`) },66  osm_way: { label: "OpenStreetMap way", href: (v) => `https://www.openstreetmap.org/way/${v}` },67  osm_node: { label: "OpenStreetMap node", href: (v) => `https://www.openstreetmap.org/node/${v}` },68};6970function externalIdRows(ids: Record<string, string | number>) {71  return Object.entries(ids)72    .filter(([, v]) => v !== null && v !== undefined && v !== "")73    .map(([k, v]) => {74      const spec = EXTERNAL_ID_LINKS[k];75      const value = String(v);76      const label = spec?.label ?? k.replace(/_/g, " ");77      return { label, value: spec ? <ExtLink href={spec.href(value)}>{value.startsWith("http") ? decodeURIComponent(value.split("/").filter(Boolean).pop() ?? value).replace(/_/g, " ") : value}</ExtLink> : <span className="figure text-[12.5px]">{value}</span> };78    });79}8081function completenessTone(c: number): string {82  return c >= 70 ? "bg-ok" : c >= 40 ? "bg-ink-3" : "bg-warn";83}8485/** One header figure: caps label, big value (or badge), note line. */86function HeaderFigure({ label, children, note, className }: { label: ReactNode; children: ReactNode; note?: ReactNode; className?: string }) {87  return (88    <div className={`min-w-0 px-3 py-3 sm:px-4 sm:first:pl-0 ${className ?? ""}`}>89      <div className="label-caps mb-1 truncate">{label}</div>90      <div className="min-w-0">{children}</div>91      {note && <div className="mt-1 text-[11px] leading-tight text-ink-3">{note}</div>}92    </div>93  );94}9596/** MW figure for the header: value + unit in mono, estimate mark, semantics + scope underneath; honest "Not published". */97function MwFigure({ field, aliases, mw, estimate, semantics, scope, planned, fallbackNote }: { field: string; aliases?: string[]; mw: number | null; estimate?: boolean; semantics: string; scope?: string | null; planned?: boolean; fallbackNote: string }) {98  const s = splitMw(mw);99  if (!s) {100    return (101      <Evidence field={field} fields={aliases} display="—" block>102        <div className="figure text-[22px] font-medium leading-none tracking-[-0.03em] text-ink-4 sm:text-[26px]">—</div>103        <div className="mt-1 text-[11px] leading-tight text-ink-3">{fallbackNote}</div>104      </Evidence>105    );106  }107  return (108    <Evidence field={field} fields={aliases} display={`${estimate || planned ? "≈ " : ""}${fmtMw(mw)}`} block>109      <div className="flex items-baseline gap-1 leading-none">110        {(estimate || planned) && <EstimateMark className="text-[14px]" />}111        <span className="figure text-[22px] font-medium tracking-[-0.03em] text-ink sm:text-[26px]">{s.value}</span>112        <span className="text-[12px] font-medium text-ink-3">{s.unit}</span>113      </div>114      <div className="mt-1 flex flex-wrap items-center gap-1.5">115        <SemanticsLabel semantics={semantics} />116        {scope && <ScopeBadge scope={scope} />}117        {estimate && <span className="text-[11px] text-ink-3">estimate</span>}118      </div>119    </Evidence>120  );121}122123function eventDate(e: EventDTO): string {124  return e.effectiveDate ?? e.detectedAt;125}126127/* ------------------------------------------------------------------ page */128129export default async function FacilityPage({ params }: Params) {130  const { slug } = await params;131  const r = await getFacility(slug);132133  if (!r.ok) {134    return (135      <Page className="py-6">136        <Breadcrumbs items={[{ name: "Data centers", href: routes.datacenters() }, { name: slug, href: routes.facility(slug) }]} />137        <OfflineNotice detail={r.error} className="mt-4" />138        <ErrorState className="mt-4" detail={r.error} />139      </Page>140    );141  }142143  const f = r.data;144  const prov = f.provenance ?? [];145  const claims = f.claims ?? [];146  const history = f.capacityHistory ?? [];147  const est = mwIsEstimate(f);148  const hasGeo = f.lat !== null && f.lng !== null;149  const imprecise = isImprecise(f.geoPrecision);150  const cHref = await countryHref(f.countryIso2, f.countryName);151  const place = placeLine(f);152  const best = bestMw(f);153  const isCampus = f.recordScope === "campus" || f.buildings.length > 0;154  const parent = f.campus ?? f.parentFacility ?? null;155  const power = f.powerContext;156  const dq = f.dataQuality;157158  const crumbs: Crumb[] = [{ name: "Data centers", href: routes.datacenters() }];159  if (f.metro) crumbs.push({ name: f.metro.name, href: routes.market(f.metro.slug) });160  else if (cHref && f.countryName) crumbs.push({ name: f.countryName, href: cHref });161  if (parent) crumbs.push({ name: parent.name, href: routes.facility(parent.slug) });162  crumbs.push({ name: f.name, href: routes.facility(f.slug) });163164  const connectivityEmpty = !f.carriers.length && !f.cloudProviders.length && !f.ixps.length && !f.carriersCount && !f.ixpCount && !f.networksCount;165  const powerEmpty = f.utilityCapacityMw == null && f.gridConnectionMw == null && !power?.powerEvents.length && !power?.gridConstraints.length;166  const tenants = f.tenants.filter((t) => t.role !== "carrier");167  const carrierTenants = f.tenants.filter((t) => t.role === "carrier");168169  // timeline: dated milestones + status / capacity events with an effective date170  const timeline: TimelineEntry[] = [];171  if (f.announcedOn) timeline.push({ date: f.announcedOn, title: "Announced", tone: "accent", meta: <WithProvenance items={prov} fields={["announcedOn", "announced_on"]}>as published</WithProvenance> });172  if (f.constructionStartedOn) timeline.push({ date: f.constructionStartedOn, title: "Construction started", tone: "warn", meta: <WithProvenance items={prov} fields={["constructionStartedOn", "construction_started_on"]}>as published</WithProvenance> });173  if (f.openedOn) timeline.push({ date: f.openedOn, title: "Opened", tone: "ok", meta: <WithProvenance items={prov} fields={["openedOn", "opened_on"]}>as published</WithProvenance> });174  for (const e of f.events.filter((e) => ["status_changed", "capacity_changed", "planned_capacity_changed", "opening_date_changed", "operator_changed", "owner_changed", "expansion_announced", "closure"].includes(e.eventType)).slice(0, 20)) {175    timeline.push({ date: eventDate(e), title: EVENT_LABEL[e.eventType], description: e.title !== f.name ? e.title : undefined, meta: <span>{e.sourceName}</span>, href: e.url, tone: e.eventType === "closure" ? "danger" : "neutral" });176  }177  timeline.push({ date: f.firstSeen, title: <span className="text-ink-2">First indexed</span>, tone: "neutral", meta: <span>{fmtInt(f.sourceCount ?? dq.sourceCount)} sources since</span> });178  if (f.lastVerified) timeline.push({ date: f.lastVerified, title: <span className="text-ink-2">Last verified against a primary source</span>, tone: "ok" });179  timeline.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));180181  const sections = [182    { id: "overview", label: "Overview" },183    { id: "capacity", label: "Capacity" },184    { id: "timeline", label: "Timeline", count: timeline.length },185    { id: "ownership", label: "Operator / ownership" },186    { id: "connectivity", label: "Connectivity" },187    { id: "power", label: "Power" },188    { id: "cloud", label: "Cloud / tenants" },189    { id: "phases", label: "Project phases", count: f.projects.length },190    { id: isCampus ? "buildings" : "campus", label: isCampus ? "Buildings" : "Campus", count: isCampus ? f.buildings.length : parent ? 1 : 0 },191    { id: "nearby", label: "Nearby", count: hasGeo ? 1 : 0 },192    { id: "events", label: "Event history", count: f.events.length },193    { id: "evidence", label: "Source evidence", count: prov.length + claims.length },194    { id: "quality", label: "Data quality" },195  ];196197  const jsonLd = compactJsonLd({198    "@context": "https://schema.org",199    "@type": "Place",200    "@id": `${absoluteUrl(routes.facility(f.slug))}#place`,201    name: f.name,202    alternateName: f.aliases.length ? f.aliases : undefined,203    description: f.description ?? undefined,204    url: absoluteUrl(routes.facility(f.slug)),205    sameAs: [f.website, ...Object.entries(f.externalIds).map(([k, v]) => (EXTERNAL_ID_LINKS[k] ? EXTERNAL_ID_LINKS[k].href(String(v)) : null))].filter(Boolean),206    address: compactJsonLd({ "@type": "PostalAddress", streetAddress: f.address ?? undefined, addressLocality: f.city ?? undefined, addressRegion: f.regionName ?? undefined, postalCode: f.postalCode ?? undefined, addressCountry: f.countryIso2 ?? undefined }),207    geo: hasGeo && f.geoPrecision !== "unknown" ? { "@type": "GeoCoordinates", latitude: f.lat, longitude: f.lng } : undefined,208    containedInPlace: parent ? { "@type": "Place", name: parent.name, url: absoluteUrl(routes.facility(parent.slug)) } : f.metro ? { "@type": "Place", name: f.metro.name, url: absoluteUrl(routes.market(f.metro.slug)) } : undefined,209    additionalProperty: [210      { "@type": "PropertyValue", name: "status", value: STATUS_LABEL[f.status] },211      { "@type": "PropertyValue", name: "facilityType", value: TYPE_LABEL[f.facilityType] },212      f.itCapacityMw !== null ? { "@type": "PropertyValue", name: "IT capacity", value: f.itCapacityMw, unitText: "MW" } : null,213      f.totalPowerMw !== null ? { "@type": "PropertyValue", name: "Total power", value: f.totalPowerMw, unitText: "MW" } : null,214      f.plannedPowerMw !== null ? { "@type": "PropertyValue", name: "Planned power", value: f.plannedPowerMw, unitText: "MW" } : null,215      { "@type": "PropertyValue", name: "geoPrecision", value: PRECISION_LABEL[f.geoPrecision] },216    ].filter(Boolean),217  });218219  const opLink = (o: { slug: string; name: string } | null | undefined, empty: string) =>220    o ? (221      <Link href={routes.operator(o.slug)} className="text-ink hover:text-accent hover:underline">222        {o.name}223      </Link>224    ) : (225      <span className="text-[12.5px] text-ink-3">{empty}</span>226    );227228  return (229    <EvidenceProvider data={{ entityName: f.name, entityKind: "facility", claims, provenance: prov, history }}>230      <Page className="pb-8">231        <JsonLd data={jsonLd} />232        <div className="pt-3">233          <Breadcrumbs items={crumbs} />234        </div>235236        {/* ---------------------------------------------------------------- header */}237        <header className="py-4 sm:py-6">238          <div className="label-caps mb-1.5 flex flex-wrap items-center gap-x-2 gap-y-1">239            <span>{isCampus ? "Data center campus" : f.recordScope === "building" ? "Data center building" : "Data center"}</span>240            {f.operator && (241              <>242                <span aria-hidden className="text-ink-4">·</span>243                <Link href={routes.operator(f.operator.slug)} className="normal-case tracking-normal text-[12.5px] font-medium text-ink-2 hover:text-accent hover:underline">244                  {f.operator.name}245                </Link>246              </>247            )}248            {parent && (249              <>250                <span aria-hidden className="text-ink-4">·</span>251                <span className="normal-case tracking-normal text-[12px] text-ink-3">252                  part of{" "}253                  <Link href={routes.facility(parent.slug)} className="text-ink-2 hover:text-accent hover:underline">254                    {parent.name}255                  </Link>256                </span>257              </>258            )}259          </div>260          <h1 className="text-[22px] font-semibold leading-tight tracking-[-0.02em] text-ink sm:text-[30px]">261            <WithProvenance items={prov} fields="name">262              {f.name}263            </WithProvenance>264          </h1>265          {place !== "—" && (266            <p className="mt-1 flex flex-wrap items-center gap-x-1.5 text-[13.5px] text-ink-2">267              <CountryFlag iso2={f.countryIso2} size={13} />268              {f.city && <span>{f.city}</span>}269              {f.metro && (270                <>271                  <span aria-hidden className="text-ink-4">·</span>272                  <Link href={routes.market(f.metro.slug)} className="hover:text-accent hover:underline">273                    {f.metro.name} market274                  </Link>275                </>276              )}277              {f.countryName && (278                <>279                  <span aria-hidden className="text-ink-4">·</span>280                  {cHref ? (281                    <Link href={cHref} className="hover:text-accent hover:underline">282                      {f.countryName}283                    </Link>284                  ) : (285                    <span>{f.countryName}</span>286                  )}287                </>288              )}289            </p>290          )}291          <div className="mt-3 flex flex-wrap items-center gap-1.5">292            <TypeBadge type={f.facilityType} />293            {f.isAi ? <AiMark evidence={f.aiEvidence} size="sm" /> : null}294            <FlagBadges isHyperscale={f.isHyperscale} size="sm" />295            {f.recordScope && <ScopeBadge scope={f.recordScope} size="sm" short={false} />}296            <span className="inline-flex h-[22px] items-center gap-1.5 rounded-xs border border-hair-2 px-2 text-[11.5px] text-ink-2" title={`Completeness ${f.completeness}/100 — share of tracked fields with a sourced value`}>297              <span className="inline-block h-[4px] w-10 overflow-hidden rounded-[1px] bg-surface-3" aria-hidden>298                <span className={`block h-full ${completenessTone(f.completeness)}`} style={{ width: `${Math.max(2, Math.min(100, f.completeness))}%` }} />299              </span>300              <span className="figure">{f.completeness}%</span>301              <span className="text-ink-3">complete</span>302            </span>303          </div>304          {f.aliases.length > 0 && (305            <p className="mt-2 text-[12px] text-ink-3">306              Also known as <span className="text-ink-2">{f.aliases.slice(0, 6).join(", ")}</span>307              {f.aliases.length > 6 && ` and ${f.aliases.length - 6} more`}308            </p>309          )}310        </header>311312        {/* ---------------------------------------------------------------- header figures (each opens the evidence drawer) */}313        <div className="grid grid-cols-2 border-y border-hair [&>*]:min-w-0 max-sm:[&>*:nth-child(even)]:border-l max-sm:[&>*:nth-child(n+3)]:border-t max-sm:[&>*:nth-child(odd)]:pl-0 max-sm:[&>*:last-child:nth-child(odd)]:col-span-2 max-sm:[&>*]:border-hair sm:grid-cols-4 sm:[&>*:nth-child(n+5)]:border-t sm:[&>*:nth-child(4n+1)]:pl-0 sm:[&>*:not(:nth-child(4n+1))]:border-l sm:[&>*]:border-hair xl:grid-cols-7 xl:[&>*:nth-child(n+5)]:border-t-0 xl:[&>*:nth-child(4n+1)]:pl-4 xl:[&>*:first-child]:pl-0 xl:[&>*+*]:border-l">314          <HeaderFigure label="Status">315            <Evidence field="status" display={STATUS_LABEL[f.status]}>316              <StatusBadge status={f.status} />317            </Evidence>318            {f.openedOn && <div className="figure mt-1 text-[11px] text-ink-3">opened {fmtDate(f.openedOn)}</div>}319          </HeaderFigure>320          <HeaderFigure label="Known IT capacity">321            <MwFigure field="itCapacityMw" aliases={["it_capacity_mw", "critical_power_mw"]} mw={f.itCapacityMw} estimate={est && best.kind === "it"} semantics={f.capacitySemantics && best.kind === "it" ? f.capacitySemantics : "it_capacity_mw"} scope={best.kind === "it" ? f.capacityScope : null} fallbackNote="Not published by any source" />322          </HeaderFigure>323          <HeaderFigure label="Total power">324            <MwFigure field="totalPowerMw" aliases={["total_power_mw", "current_power_mw"]} mw={f.totalPowerMw} estimate={est && best.kind === "total"} semantics={f.capacitySemantics && best.kind === "total" ? f.capacitySemantics : "current_power_mw"} scope={best.kind === "total" ? f.capacityScope : null} fallbackNote="Not published by any source" />325          </HeaderFigure>326          <HeaderFigure label="Planned build-out">327            <MwFigure field="plannedPowerMw" aliases={["planned_power_mw", "phase_mw", "plannedMw"]} mw={f.plannedPowerMw ?? f.ultimateCampusMw ?? null} planned semantics={f.plannedPowerMw !== null ? "planned_power_mw" : "ultimate_campus_mw"} scope={best.kind === "planned" ? f.capacityScope : null} fallbackNote={f.status === "operational" ? "No expansion figure published" : "Not published by any source"} />328          </HeaderFigure>329          <HeaderFigure label="Confidence" note={`${fmtInt(dq.sourceCount)} ${dq.sourceCount === 1 ? "source" : "sources"} · ${fmtInt(dq.primarySourceCount)} primary`}>330            <Evidence field="confidence" fields={["status", "itCapacityMw", "totalPowerMw"]} display={CONFIDENCE_LABEL[f.confidence]}>331              <ConfidenceBadge level={f.confidence} short={false} />332            </Evidence>333          </HeaderFigure>334          <HeaderFigure label="Location precision" note={hasGeo ? (imprecise ? "ring marks the area, not the building" : `${f.lat!.toFixed(4)}, ${f.lng!.toFixed(4)}`) : "no coordinates published"}>335            <Evidence field="geoPrecision" fields={["geo_precision", "lat", "lng", "geo", "coordinates"]} display={PRECISION_LABEL[f.geoPrecision]}>336              <PrecisionBadge precision={f.geoPrecision} short={false} />337            </Evidence>338          </HeaderFigure>339          <HeaderFigure label="Last verified" note={f.lastVerified ? "against a primary source" : "no primary-source verification yet"}>340            <Evidence field="lastVerified" fields={["last_verified"]} display={f.lastVerified ? fmtDate(f.lastVerified) : "—"} block>341              <div className={`figure text-[18px] font-medium leading-none tracking-[-0.02em] ${f.lastVerified ? "text-ink" : "text-ink-4"}`}>{f.lastVerified ? fmtDate(f.lastVerified) : "—"}</div>342            </Evidence>343          </HeaderFigure>344        </div>345        <p className="mt-2 text-[11px] text-ink-4">Figures are published values only (≈ marks estimates and planned figures); click any figure for its sources, sentence-level evidence and history. Nothing is imputed.</p>346347        <div className="mt-4">348          <SectionNav items={sections} />349        </div>350351        <div className="flex flex-col gap-9 pt-6">352          {/* ------------------------------------------------------------ overview */}353          <Section id="overview" title="Overview" className="scroll-mt-28">354            <div className="grid gap-6 md:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)]">355              <div className="min-w-0">356                {f.description ? (357                  <p className="text-[14px] leading-relaxed text-ink-2">358                    <WithProvenance items={prov} fields="description">359                      <span>{f.description}</span>360                    </WithProvenance>361                  </p>362                ) : (363                  <NotPublished what="Description" />364                )}365                <KeyValueList366                  className="mt-4"367                  columns={2}368                  items={[369                    { label: "Type", value: <TypeBadge type={f.facilityType} size="xs" /> },370                    { label: "Tier", value: f.tier ? <WithProvenance items={prov} fields="tier">{f.tier}</WithProvenance> : null },371                    { label: "Building", value: f.buildingSqm !== null ? <Evidence field="buildingSqm" fields={["building_sqm"]}><span className="figure">{fmtInt(f.buildingSqm)} m²</span></Evidence> : null },372                    { label: "Site area", value: f.siteAreaHa !== null ? <Evidence field="siteAreaHa" fields={["site_area_ha"]}><span className="figure">{fmtInt(f.siteAreaHa)} ha</span></Evidence> : null },373                    { label: "Racks", value: f.rackCount !== null ? <Evidence field="rackCount" fields={["rack_count"]}><span className="figure">{fmtInt(f.rackCount)}</span></Evidence> : null },374                    { label: "PUE", value: f.pue !== null ? <Evidence field="pue"><span className="figure">{f.pue.toFixed(2)}</span></Evidence> : null },375                    { label: "Cooling", value: f.coolingType ? <WithProvenance items={prov} fields={["coolingType", "cooling_type"]}>{f.coolingType}</WithProvenance> : null },376                    { label: "Renewable claim", value: f.renewableClaim ? <WithProvenance items={prov} fields={["renewableClaim", "renewable_claim"]}>{f.renewableClaim}</WithProvenance> : null },377                    { label: "Website", value: f.website ? <WithProvenance items={prov} fields="website"><ExtLink href={f.website} /></WithProvenance> : null },378                    ...externalIdRows(f.externalIds),379                    { label: "First indexed", value: <span className="figure text-[12.5px] text-ink-2">{fmtDate(f.firstSeen)}</span> },380                    { label: "Record updated", value: <span className="figure text-[12.5px] text-ink-2">{fmtDate(f.updatedAt)}</span> },381                  ]}382                />383                {f.certifications.length > 0 && (384                  <div className="mt-4">385                    <div className="label-caps mb-1.5 flex items-center gap-1">386                      Certifications <ProvenancePopover items={provFor(prov, "certifications")} size={12} />387                    </div>388                    <div className="flex flex-wrap gap-1.5">389                      {f.certifications.map((c) => (390                        <Badge key={c} tone="outline">391                          {c}392                        </Badge>393                      ))}394                    </div>395                  </div>396                )}397              </div>398              <div className="min-w-0">399                {hasGeo ? (400                  <EntityMap center={{ lat: f.lat!, lng: f.lng! }} precision={f.geoPrecision} filters={f.countryIso2 ? { country: f.countryIso2 } : undefined} className="h-[200px] sm:h-[260px]" openLabel="Open in map" />401                ) : (402                  <div className="flex min-h-[140px] items-center justify-center rounded-sm border border-dashed border-hair-2 text-[12.5px] text-ink-3">No coordinates published — not drawn on the map.</div>403                )}404                <KeyValueList405                  className="mt-3"406                  items={[407                    { label: "Address", value: f.address ? <WithProvenance items={prov} fields="address">{f.address}</WithProvenance> : null },408                    { label: "Postal code", value: f.postalCode ? <span className="figure">{f.postalCode}</span> : null },409                    { label: "City", value: f.city ? <WithProvenance items={prov} fields="city">{f.city}</WithProvenance> : null },410                    { label: "Region", value: f.regionName ? <WithProvenance items={prov} fields={["regionName", "region_name"]}>{f.regionName}</WithProvenance> : null },411                    { label: "Market", value: f.metro ? <Link href={routes.market(f.metro.slug)} className="hover:text-accent hover:underline">{f.metro.name}</Link> : null },412                    { label: "Country", value: f.countryIso2 ? <span className="inline-flex items-center gap-1.5"><CountryFlag iso2={f.countryIso2} size={13} />{cHref ? <Link href={cHref} className="hover:text-accent hover:underline">{f.countryName ?? f.countryIso2}</Link> : <span>{f.countryName ?? f.countryIso2}</span>}</span> : null },413                    { label: "Coordinates", value: hasGeo ? <Evidence field="lat" fields={["lng", "geo", "coordinates", "geoPrecision"]}><span className="figure text-[12.5px]">{f.lat!.toFixed(4)}, {f.lng!.toFixed(4)}</span></Evidence> : null },414                    { label: "Geo source", value: f.geoSource ? <span className="figure text-[12px] text-ink-3">{f.geoSource}</span> : null },415                  ]}416                />417                {!f.address && !hasGeo && <NotPublished what="Address and coordinates" className="mt-2" />}418                {hasGeo && imprecise && <p className="mt-2 text-[12px] text-ink-3">Location shown at {PRECISION_LABEL[f.geoPrecision].toLowerCase().replace(" location", "")}: the exact site has not been published by the indexed sources; the ring marks the area, not the building.</p>}419              </div>420            </div>421          </Section>422423          {/* ------------------------------------------------------------ capacity */}424          <Section id="capacity" title="Capacity" className="scroll-mt-28" description="Every capacity figure published about this record, by what it measures (IT load ≠ total power ≠ planned ≠ grid connection), its scope and its source. Only site-scoped figures populate the columns above.">425            <div className="grid gap-6 lg:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]">426              <div className="min-w-0">427                <div className="label-caps mb-2">Capacity claims</div>428                <ClaimsTable claims={claims} kind="capacity" emptyNote={best.mw !== null ? "No sentence-level capacity claim yet — the figures above come from structured source fields listed under Source evidence." : "No source has published a capacity figure for this record."} />429                {(f.utilityCapacityMw != null || f.gridConnectionMw != null || f.ultimateCampusMw != null) && (430                  <KeyValueList431                    className="mt-4"432                    items={[433                      { label: "Utility capacity", value: f.utilityCapacityMw != null ? <Evidence field="utilityCapacityMw"><span className="figure">{fmtMw(f.utilityCapacityMw)}</span> <SemanticsLabel semantics="utility_capacity_mw" /></Evidence> : null },434                      { label: "Grid connection", value: f.gridConnectionMw != null ? <Evidence field="gridConnectionMw"><span className="figure">{fmtMw(f.gridConnectionMw)}</span> <SemanticsLabel semantics="grid_connection_mw" /></Evidence> : null },435                      { label: "Ultimate build-out", value: f.ultimateCampusMw != null ? <Evidence field="ultimateCampusMw"><EstimateMark className="mr-0.5" /><span className="figure">{fmtMw(f.ultimateCampusMw)}</span> <SemanticsLabel semantics="ultimate_campus_mw" /></Evidence> : null },436                    ]}437                  />438                )}439              </div>440              <div className="min-w-0">441                <div className="label-caps mb-2">Capacity history</div>442                <CapacityHistoryChart points={history} height={170} />443              </div>444            </div>445          </Section>446447          {/* ------------------------------------------------------------ timeline */}448          <Section id="timeline" title="Timeline" className="scroll-mt-28" description="Dated milestones and detected changes, oldest first. Partial dates are shown as published.">449            <TimelineList items={timeline} dense />450          </Section>451452          {/* ------------------------------------------------------------ operator / ownership */}453          <Section id="ownership" title="Operator / ownership" className="scroll-mt-28" description="Operator, owner, developer, landowner and tenants are distinct roles and are never merged.">454            <KeyValueList455              columns={2}456              items={[457                { label: "Operator", value: <WithProvenance items={prov} fields={["operator", "operatorName"]}>{opLink(f.operator, "Not published")}</WithProvenance>, keepEmpty: true },458                { label: "Owner", value: f.owner ? <WithProvenance items={prov} fields={["owner", "ownerName"]}>{opLink(f.owner, "")}{f.owner.id === f.operator?.id && <span className="ml-1.5 text-[11.5px] text-ink-3">(same as operator)</span>}</WithProvenance> : <span className="text-[12.5px] text-ink-3">Not published</span>, keepEmpty: true },459                { label: "Developer", value: opLink(f.developer, "Not published"), keepEmpty: true },460                { label: "Landowner", value: opLink(f.landowner, "Not published"), keepEmpty: true },461                {462                  label: "Tenants",463                  value: tenants.length ? (464                    <ul className="flex flex-wrap gap-1.5">465                      {tenants.map((t) => (466                        <li key={t.id}>467                          <Link href={routes.operator(t.slug)} className="inline-flex h-7 items-center gap-1.5 rounded-xs border border-hair px-2 text-[12px] text-ink hover:text-accent">468                            {t.name} <span className="text-ink-4">{t.role}</span>469                          </Link>470                        </li>471                      ))}472                    </ul>473                  ) : (474                    <span className="text-[12.5px] text-ink-3">No tenant published{carrierTenants.length ? ` (carriers are listed under Connectivity)` : ""}</span>475                  ),476                  keepEmpty: true,477                },478              ]}479            />480            {f.events.some((e) => e.eventType === "operator_changed" || e.eventType === "owner_changed" || e.eventType === "acquisition") && (481              <div className="mt-4">482                <div className="label-caps mb-1.5">Ownership changes</div>483                <EventList events={f.events.filter((e) => e.eventType === "operator_changed" || e.eventType === "owner_changed" || e.eventType === "acquisition")} dense />484              </div>485            )}486          </Section>487488          {/* ------------------------------------------------------------ connectivity */}489          <Section id="connectivity" title="Connectivity" className="scroll-mt-28" description="Carriers, cloud on-ramps and internet exchanges present at this site, as published by the operator or PeeringDB.">490            {connectivityEmpty ? (491              <NotPublished what="Connectivity (carriers, cloud on-ramps, IXPs)" />492            ) : (493              <div className="grid gap-4 sm:grid-cols-3">494                <div>495                  <div className="label-caps mb-1.5">496                    Carriers {f.carriersCount !== null && f.carriersCount !== undefined && <span className="figure text-ink-3">{fmtInt(f.carriersCount)}</span>}497                    {f.networksCount !== null && f.networksCount !== undefined && <span className="ml-2 normal-case tracking-normal text-ink-4">{fmtInt(f.networksCount)} networks</span>}498                  </div>499                  {f.carriers.length ? (500                    <ul className="divide-y divide-hair text-[13px]">501                      {f.carriers.map((c) => (502                        <li key={c.id} className="flex items-baseline justify-between gap-2 py-1.5">503                          <Link href={routes.operator(c.slug)} className="truncate text-ink hover:text-accent hover:underline">504                            {c.name}505                          </Link>506                          {c.asn && <span className="figure shrink-0 text-[11.5px] text-ink-3">AS{c.asn}</span>}507                        </li>508                      ))}509                    </ul>510                  ) : carrierTenants.length ? (511                    <ul className="divide-y divide-hair text-[13px]">512                      {carrierTenants.map((c) => (513                        <li key={c.id} className="py-1.5">514                          <Link href={routes.operator(c.slug)} className="text-ink hover:text-accent hover:underline">515                            {c.name}516                          </Link>517                        </li>518                      ))}519                    </ul>520                  ) : (521                    <p className="text-[12.5px] text-ink-3">{f.carriersCount ? "Names not published; count from PeeringDB." : "None listed."}</p>522                  )}523                </div>524                <div>525                  <div className="label-caps mb-1.5">Cloud on-ramps</div>526                  {f.cloudProviders.length ? (527                    <ul className="divide-y divide-hair text-[13px]">528                      {f.cloudProviders.map((c) => (529                        <li key={c.id} className="py-1.5">530                          <Link href={routes.operator(c.slug)} className="text-ink hover:text-accent hover:underline">531                            {c.name}532                          </Link>533                        </li>534                      ))}535                    </ul>536                  ) : (537                    <p className="text-[12.5px] text-ink-3">None listed.</p>538                  )}539                </div>540                <div>541                  <div className="label-caps mb-1.5">542                    Internet exchanges {f.ixpCount !== null && f.ixpCount !== undefined && <span className="figure text-ink-3">{fmtInt(f.ixpCount)}</span>}543                  </div>544                  {f.ixps.length ? (545                    <ul className="divide-y divide-hair text-[13px]">546                      {f.ixps.map((x) => (547                        <li key={x.id} className="py-1.5">548                          <Link href={routes.ixp(x.slug)} className="text-ink hover:text-accent hover:underline">549                            {x.name}550                          </Link>551                        </li>552                      ))}553                    </ul>554                  ) : (555                    <p className="text-[12.5px] text-ink-3">{f.ixpCount ? "Names not published; count from PeeringDB." : "None listed."}</p>556                  )}557                </div>558              </div>559            )}560          </Section>561562          {/* ------------------------------------------------------------ power */}563          <Section id="power" title="Power" className="scroll-mt-28" description="Utility-side figures (power available or contracted from the grid) are never compared with IT load. Grid events and constraints are attached to the market or country, not to the site.">564            <div className="grid gap-6 md:grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)]">565              <div className="min-w-0">566                <KeyValueList567                  items={[568                    { label: "Utility capacity", value: f.utilityCapacityMw != null ? <Evidence field="utilityCapacityMw"><span className="figure">{fmtMw(f.utilityCapacityMw)}</span></Evidence> : <span className="text-[12.5px] text-ink-3">Not published</span>, keepEmpty: true },569                    { label: "Grid connection", value: f.gridConnectionMw != null ? <Evidence field="gridConnectionMw"><span className="figure">{fmtMw(f.gridConnectionMw)}</span></Evidence> : <span className="text-[12.5px] text-ink-3">Not published</span>, keepEmpty: true },570                    { label: "Renewable claim", value: f.renewableClaim ?? <span className="text-[12.5px] text-ink-3">Not published</span>, keepEmpty: true },571                  ]}572                />573                {power?.countryEnergy && (f.countryName || f.countryIso2) && (574                  <div className="mt-4">575                    <div className="label-caps mb-1.5">Country energy context · {f.countryName ?? f.countryIso2}</div>576                    <div className="grid grid-cols-2 gap-3">577                      <div>578                        <div className="figure text-[18px] font-medium text-ink">{power.countryEnergy.renewableShare !== null ? fmtPct(power.countryEnergy.renewableShare / 100) : "—"}</div>579                        <div className="text-[11px] text-ink-3">renewable share of generation{power.countryEnergy.statsYear ? ` · ${power.countryEnergy.statsYear}` : ""}</div>580                      </div>581                      <div>582                        <div className="figure text-[18px] font-medium text-ink">{power.countryEnergy.electricityTwh !== null ? `${fmtInt(power.countryEnergy.electricityTwh)} TWh` : "—"}</div>583                        <div className="text-[11px] text-ink-3">national generation{power.countryEnergy.statsYear ? ` · ${power.countryEnergy.statsYear}` : ""}</div>584                      </div>585                    </div>586                    <p className="mt-2 text-[11px] text-ink-4">National grid averages — they do not describe this facility&apos;s contracted electricity or power purchase agreements.</p>587                  </div>588                )}589              </div>590              <div className="min-w-0">591                <div className="label-caps mb-1.5">Power events</div>592                {power?.powerEvents.length ? <EventList events={power.powerEvents} dense /> : <p className="text-[12.5px] text-ink-3">No power agreement or utility event linked to this facility.</p>}593                <div className="label-caps mb-1.5 mt-4">Grid constraints in this market</div>594                {power?.gridConstraints.length ? (595                  <ul className="divide-y divide-hair">596                    {power.gridConstraints.map((g) => (597                      <li key={g.id} className="py-2 text-[13px]">598                        <div className="flex flex-wrap items-center gap-1.5">599                          <Badge tone="outline" size="xs">600                            {g.kind.replace(/_/g, " ")}601                          </Badge>602                          {g.sourceKind && <SourceBadge kind={g.sourceKind} name={g.sourceName} url={g.url} />}603                          <span className="figure ml-auto text-[11.5px] text-ink-3">{fmtDate(g.effectiveDate)}</span>604                        </div>605                        <a href={g.url} target="_blank" rel="noopener noreferrer" className="mt-1 inline-flex items-center gap-1 font-medium text-ink hover:text-accent">606                          {g.title} <ExternalLink className="size-3" />607                        </a>608                        {g.summary && <p className="mt-0.5 text-[12.5px] text-ink-2">{g.summary}</p>}609                      </li>610                    ))}611                  </ul>612                ) : (613                  <p className="text-[12.5px] text-ink-3">No public grid constraint reported for this market.</p>614                )}615                {powerEmpty && power?.note && <p className="mt-3 text-[11px] text-ink-4">{power.note}</p>}616              </div>617            </div>618          </Section>619620          {/* ------------------------------------------------------------ cloud / tenants */}621          <Section id="cloud" title="Cloud / tenants" className="scroll-mt-28" description={f.cloudRegions.length ? "Public cloud regions published for the same metro. Providers publish the metro, not the building — presence here does not imply this facility hosts them." : "Cloud providers publish regions per metro, not per building."} aside={<Link href={routes.cloudRegions()} className="text-[12.5px] text-ink-3 hover:text-ink">All cloud regions →</Link>}>622            {f.cloudRegions.length ? (623              <Table dense>624                <THead>625                  <tr>626                    <Th>Provider</Th>627                    <Th>Code</Th>628                    <Th className="hidden sm:table-cell">Name</Th>629                    <Th numeric className="hidden md:table-cell">630                      AZs631                    </Th>632                    <Th className="hidden md:table-cell">Launched</Th>633                    <Th>Status</Th>634                  </tr>635                </THead>636                <TBody>637                  {f.cloudRegions.map((cr) => (638                    <Tr key={cr.id}>639                      <Td primary>640                        <Link href={routes.operator(cr.provider.slug)} className="font-medium text-ink hover:text-accent hover:underline">641                          {cr.provider.name}642                        </Link>643                        <span className="ml-2 text-[12px] text-ink-3 sm:hidden">{cr.name}</span>644                      </Td>645                      <Td label="Code" className="figure text-[12.5px]">646                        <RowLink href={routes.cloudRegion(cr.slug)} className="font-normal">647                          {cr.code}648                        </RowLink>649                      </Td>650                      <Td label="Name" className="hidden text-ink-2 sm:table-cell">651                        {cr.name}652                      </Td>653                      <Td label="AZs" numeric className="hidden md:table-cell">654                        {cr.availabilityZones ?? "—"}655                      </Td>656                      <Td label="Launched" className="figure hidden text-[12.5px] text-ink-3 md:table-cell">657                        {fmtDate(cr.launchedOn)}658                      </Td>659                      <Td label="Status">660                        <Badge tone={cr.status === "operational" ? "ok" : cr.status === "announced" ? "info" : cr.status === "restricted" ? "warn" : "neutral"} size="xs">661                          {cr.status}662                        </Badge>663                        {cr.isSovereign && (664                          <Badge tone="accent" size="xs" className="ml-1">665                            Sovereign666                          </Badge>667                        )}668                      </Td>669                    </Tr>670                  ))}671                </TBody>672              </Table>673            ) : (674              <p className="text-[12.5px] text-ink-3">No public cloud region is published for this market.</p>675            )}676            {tenants.length > 0 && (677              <p className="mt-3 text-[12.5px] text-ink-2">678                Published tenants: {tenants.map((t, i) => (679                  <span key={t.id}>680                    {i > 0 && ", "}681                    <Link href={routes.operator(t.slug)} className="hover:text-accent hover:underline">682                      {t.name}683                    </Link>684                  </span>685                ))}686                .687              </p>688            )}689          </Section>690691          {/* ------------------------------------------------------------ project phases */}692          <Section id="phases" title="Project phases" className="scroll-mt-28" description="Announced or under-construction phases linked to this facility, or to the same operator in this market. Planned MW are published figures (≈).">693            <ProjectList items={f.projects} emptyTitle="No project phase linked" emptyDescription="Expansion phases appear here when a source publishes them." />694          </Section>695696          {/* ------------------------------------------------------------ buildings / campus */}697          {isCampus ? (698            <Section id="buildings" title="Buildings" className="scroll-mt-28" description="Buildings contained in this campus. A campus whose buildings publish figures contributes no MW itself; totals are never double-counted." aside={<span className="figure text-[12px] text-ink-3">{fmtInt(f.buildings.length)}</span>}>699              <FacilityTable items={f.buildings} columns={["operator", "status", "mw", "type", "confidence"]} emptyTitle="No building published yet" emptyDescription="This record is a campus; individual buildings appear once a source publishes them." />700            </Section>701          ) : (702            <Section id="campus" title="Campus" className="scroll-mt-28" description="Containment: a building's figure never stands for its campus, and a campus figure is never written to a building.">703              {parent ? (704                <p className="text-[13.5px] text-ink-2">705                  This {f.recordScope === "building" ? "building" : "facility"} is part of{" "}706                  <Link href={routes.facility(parent.slug)} className="font-medium text-ink hover:text-accent hover:underline">707                    {parent.name}708                  </Link>709                  .710                </p>711              ) : (712                <p className="text-[12.5px] text-ink-3">Not part of a published campus record.</p>713              )}714            </Section>715          )}716717          {/* ------------------------------------------------------------ nearby */}718          <Section id="nearby" title="Nearby infrastructure" className="scroll-mt-28" description="Great-circle distance between published coordinates. Proximity never implies a connection.">719            {hasGeo ? <NearbyInfrastructurePanel initial={f.nearbyInfrastructure} center={{ lat: f.lat!, lng: f.lng! }} excludeId={f.id} /> : <NotPublished what="Coordinates — nearby infrastructure" />}720          </Section>721722          {/* ------------------------------------------------------------ event history */}723          <Section id="events" title="Event history" className="scroll-mt-28" description="Detected changes to this record — status, capacity, dates, operator — with their source and significance." aside={<Link href={routes.live()} className="inline-flex items-center gap-1 text-[12.5px] text-ink-3 hover:text-ink">Live feed <ExternalLink className="size-3" /></Link>}>724            <EventList events={f.events} dense emptyTitle="No change detected yet" emptyDescription="Status, capacity and date changes will be listed here as connectors re-crawl the sources." />725            {f.sourceHistory.length > 0 && (726              <details className="mt-4 group">727                <summary className="cursor-pointer text-[12.5px] font-medium text-ink-2 hover:text-ink">728                  Source observation log <span className="figure text-ink-3">{fmtInt(f.sourceHistory.length)}</span>729                </summary>730                <div className="mt-3">731                  <SourceHistory items={[...f.sourceHistory].sort((a, b) => (a.date < b.date ? 1 : -1)).slice(0, 60)} />732                  {f.sourceHistory.length > 60 && <p className="mt-2 text-[11.5px] text-ink-4">Showing the 60 most recent of {fmtInt(f.sourceHistory.length)} entries.</p>}733                </div>734              </details>735            )}736          </Section>737738          {/* ------------------------------------------------------------ source evidence */}739          <Section id="evidence" title="Source evidence" className="scroll-mt-28" description="Every observation behind this record: field, value, source, dates, confidence and method. “Shown” marks the winner backing the displayed value." aside={<span className="figure text-[12px] text-ink-3">{fmtInt(prov.length)} observations</span>}>740            <ProvenanceTable items={prov} />741            {claims.some((c) => /_usd$/.test(c.predicate)) && (742              <div className="mt-4">743                <div className="label-caps mb-2">Investment claims</div>744                <ClaimsTable claims={claims} kind="investment" />745              </div>746            )}747            <p className="mt-2 text-[11.5px] text-ink-4">748              {CONFIDENCE_LABEL[f.confidence]} overall — computed from the best source kind and the number of independent primary sources.{" "}749              <Link href={`${routes.methodology()}#confidence`} className="underline decoration-hair-2 hover:text-ink">750                How confidence is computed751              </Link>752            </p>753          </Section>754755          {/* ------------------------------------------------------------ data quality */}756          <Section id="quality" title="Data quality" className="scroll-mt-28">757            <DataQualityPanel q={dq} />758          </Section>759        </div>760761        <SourcesFooter sources={r.sources} updatedAt={f.updatedAt} />762      </Page>763    </EvidenceProvider>764  );765}766